package org.noip.amdg;

import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.Clip;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.sound.sampled.AudioFileFormat.Type;

public class SoundPlayer implements GameEventListener, Runnable {

	private final static URL drop = SoundPlayer.class.getResource("sounds/drop.wav");
	private final static URL correct = SoundPlayer.class.getResource("sounds/correct.wav");
	private final static URL incorrect = SoundPlayer.class.getResource("sounds/incorrect.wav");
	private final static URL falling = SoundPlayer.class.getResource("sounds/falling.wav");
	private final static URL levelup = SoundPlayer.class.getResource("sounds/levelup.wav");
	private final static URL theme = SoundPlayer.class.getResource("sounds/theme.midi");
	
	private final Sequencer themeSequencer;
	
	private final AtomicBoolean music = new AtomicBoolean(true);
	
	private final Map<GameEvent.Type, SoundEffect> soundEffects = new HashMap<GameEvent.Type, SoundEffect>();
	
	private final Thread soundThread;

	public SoundPlayer()
			throws UnsupportedAudioFileException, IOException,
			LineUnavailableException, InvalidMidiDataException, MidiUnavailableException {
		
		addSoundEffect(new SoundEffect(getClip(levelup), GameEvent.Type.LEVEL_UP));
		addSoundEffect(new SoundEffect(getClip(correct), GameEvent.Type.CORRECT_ANSWER));
		addSoundEffect(new SoundEffect(getClip(incorrect), GameEvent.Type.INCORRECT_ANSWER));
		addSoundEffect(new SoundEffect(getClip(drop), GameEvent.Type.NEW_PROBLEM));
		addSoundEffect(new SoundEffect(getClip(falling), GameEvent.Type.DROP_DOWN));
		
		Sequence sequence = MidiSystem.getSequence(theme);
		themeSequencer = MidiSystem.getSequencer();
		themeSequencer.setSequence(sequence);
		themeSequencer.open();
		themeSequencer.setLoopCount(Sequencer.LOOP_CONTINUOUSLY);
		
		soundThread = new Thread(this);
		soundThread.setPriority(Thread.MIN_PRIORITY);
	}
	
	private void addSoundEffect(SoundEffect soundEffect) {
		soundEffects.put(soundEffect.eventType, soundEffect);	
	}

	private Clip getClip(URL url) throws UnsupportedAudioFileException,
			IOException, LineUnavailableException {
		Clip line;
		AudioInputStream audio;

		audio = AudioSystem.getAudioInputStream(url);

		DataLine.Info info = new DataLine.Info(Clip.class, audio.getFormat());

		if (!AudioSystem.isLineSupported(info)) {
			for (Type type : AudioSystem.getAudioFileTypes()) {
				System.out.println(type.toString());
			}
			System.err.println("Screwed!");
		}

		line = (Clip) AudioSystem.getLine(info);
		line.open(audio);
		return line;
	}

	public void play(Clip clip) {
		if (clip.isRunning()) clip.stop();
		clip.setFramePosition(0);
		clip.start();
	}
	
	public void startThread() {
		soundThread.start();
	}

	public void run() {
		themeSequencer.start();
		while (true) {
			try {
				synchronized (soundThread) {
					soundThread.wait();
				}
			} catch (InterruptedException e1) {}
			
			for (SoundEffect soundEffect : soundEffects.values()) {
				if (soundEffect.play()) {
					play(soundEffect.clip);
				}
			}
		}
	}
	
	public void toggleMusic() {
		if (!music.compareAndSet(true, false)) {
			music.set(true);
			themeSequencer.start();
		} else {
			themeSequencer.stop();
		}
	}

	public void addEvent(GameEvent event) {
		switch (event.type) {
		case PAUSE:
			if (music.get()) themeSequencer.stop();
			break;
		case UNPAUSE:
			if (music.get()) themeSequencer.start();
			break;
		default:
			SoundEffect soundEffect = soundEffects.get(event.type);
			if (soundEffect != null) {
				soundEffect.setGo();
				synchronized (soundThread) {
					soundThread.notify();	
				}
			}
		}
	}

	public void stopMusic() {
		if (music.compareAndSet(true, false)) {
			themeSequencer.stop();
		}
	}
	
	private static class SoundEffect {
		private final AtomicBoolean go = new AtomicBoolean(false);
		private final Clip clip;
		private final GameEvent.Type eventType;
		
		public SoundEffect(Clip clip, GameEvent.Type eventType) {
			this.clip = clip;
			this.eventType = eventType;
		}
		
		public void setGo() {
			go.set(true);
		}
		
		public boolean play() {
			return (go.compareAndSet(true, false));
		}
	}

}
